/* dircproxy
 * Copyright (C) 2000-2003 Scott James Remnant <scott at netsplit dot com>
 * 
 * Copyright (C) 2004-2008 Francois Harvey <contact at francoisharvey dot ca>
 * 
 * Copyright (C) 2008-2009 Noel Shrum <noel dot w8tvi at gmail dot com>
 *                         Francois Harvey <contact at francoisharvey dot ca>
 * 
 * 
 * dns.c
 *  - non-blocking DNS lookups using callbacks
 *  - wrappers around /etc/services lookup functions
 *
 * The non-blocking stuff is a little complex, but it means that the main
 * loop can continue while waiting for DNS requests to complete.  Completion
 * of a DNS request is notified by the child death signal, so it will
 * interrupt the main loop to continue where you left off.
 * --
 * @(#) $Id: dns.c,v 1.15 2002/12/29 21:30:11 scott Exp $
 *
 * This file is distributed according to the GNU General Public
 * License.  For full details, read the top of 'main.c' or the
 * file called COPYING that was distributed with this code.
 */

#include <stdio.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <netdb.h>

#include <dircproxy.h>
#include "sprintf.h"
#include "dns.h"

/* Structure used to hold information about a dns child */
struct dnschild {
  pid_t pid;
  int pipe;

  const char *ip;

  dns_fun_t function;
  void *boundto;
  void *data;

  struct dnschild *next;
};

/* Reply generated by a child */
struct dnsresult {
  int success;
  char ip[40];
  char name[DNS_MAX_HOSTLEN];
};

/* forward declarations */
static struct dnsresult _dns_lookup(const char *, const char *);
static int _dns_startrequest(void *, dns_fun_t, void *, const char *, const char *);

/* Children */
static struct dnschild *dnschildren = 0;

/* Function that looks up DNS request */
static struct dnsresult _dns_lookup(const char *name, const char *ip) {
  struct dnsresult res;
  pid_t pid;

  memset(&res, 0, sizeof(struct dnsresult));
  pid = getpid();

  if (name) {
    debug("%d: Looking up IP for '%s'", pid, name);

    if (dns_getip(name, res.ip)) {
      strncpy(res.name, name, sizeof(res.name));
      res.name[sizeof(res.name) - 1] = '\0';
      res.success = 1;
    }
  } else if (ip) {
    debug("%d: Lookup up name for '%s'", pid, ip);
    
    if (dns_getname(ip, res.name, sizeof(res.name))) {
      strncpy(res.ip, ip, sizeof(res.ip));
      res.ip[sizeof(res.ip) - 1] = '\0';
      res.success = 1;
    }
  }

  if (res.success)
    debug("%d: Got '%s' (%s)", pid, res.name, res.ip);
  else
    debug("%d: No result", pid);

  return res;
}

/* Function that starts a non-blocking DNS request. */
static int _dns_startrequest(void *boundto, dns_fun_t function, void *data,
                             const char *ip, const char *name)
{
  struct dnschild *child;
  int p[2], wp[2];

  /* Pipe where the result will be placed on success */
  if (pipe(p)) {
    syscall_fail("pipe", "p", 0);
    function(boundto, data, 0, 0);
    return -1;
  }

  /* Pipe to indicate the child can go */
  if (pipe(wp)) {
    close(p[0]);
    close(p[1]);
    syscall_fail("pipe", "wp", 0);
    function(boundto, data, 0, 0);
    return -1;
  }

  /* Allocate and place the child on the list now, to avoid race conditions */
  child = (struct dnschild *)malloc(sizeof(struct dnschild));
  child->pipe = p[0];
  child->function = function;
  child->boundto = boundto;
  child->ip = ip;
  child->data = data;
  child->next = dnschildren;
  dnschildren = child;

  /* Fork */
  child->pid = fork();
  if (child->pid == -1) {
    /* Error */
    syscall_fail("fork", 0, 0);
    dnschildren = child->next;
    close(p[1]);
    close(p[0]);
    close(wp[1]);
    close(wp[0]);
    free(child);
    function(boundto, data, 0, 0);
    return -1;
    
  } else if (child->pid) {
    /* Parent */
    debug("New DNS process started, pid %d", child->pid);
    close(p[1]);
    close(wp[0]);

    /* Send go signal */
    write(wp[1], "go", 2);
    close(wp[1]);
    return 0;

  } else {
    struct dnsresult result;
    char gobuf[2];

    close(p[0]);
    close(wp[1]);
    
    /* Use ALARM to do timeouts */
    signal(SIGALRM, SIG_DFL);
    alarm(g.dns_timeout);

    /* Wait for a go signal */
    read(wp[0], gobuf, 2);
    close(wp[0]);
    
    /* Do the lookup */
    result = _dns_lookup(name, ip);
    if (result.success) {
      /* Succeded, write to our parent and die */
      write(p[1], (void *)&result, sizeof(struct dnsresult));
      exit(0);
    } else {
      /* Didn't succeed */
      exit(1);
    }
  }
}

/* Called to end a DNS request.  0 = Not handled, >0 = Ok, <0 = Error */
int dns_endrequest(pid_t pid, int status) {
  struct dnschild *lastchild, *child;
  struct dnsresult result;
  char *ip;
  char *name;
  size_t len;

  /* Check to see whether this was a DNS child */
  child = dnschildren;
  lastchild = 0;
  while (child) {
    if (child->pid == pid)
      break;

    lastchild = child;
    child = child->next;
  }
  if (!child)
    return 0;

  /* Remove it from the list */
  if (lastchild) {
    lastchild->next = child->next;
  } else {
    dnschildren = child->next;
  }

  debug("%d: Was a DNS child, getting result", pid);

  /* Parameters to call function with */
  name = 0;
  ip = 0;
  
  /* Read from pipe if child returned normally */
  if (WIFEXITED(status)) {
    if (!WEXITSTATUS(status)) {
      len = read(child->pipe, (void *)&result, sizeof(struct dnsresult));
      if (len != sizeof(struct dnsresult)) {
        syscall_fail("read", 0, 0);
      } else if (result.success) {
        debug("%d: Got result", pid);

        ip = result.ip;
        name = result.name;
      } else {
        debug("%d: DNS lookup failed", pid);
      }
    } else {
      debug("%d: DNS lookup returned %d", pid, WEXITSTATUS(status));
    }
  } else if (WIFSIGNALED(status)) {
    debug("%d: DNS lookup terminated with signal %d", pid, WTERMSIG(status));
  } else {
    debug("%d: DNS lookup terminated abnormally", pid);
  }

  /* If DNS failed, but we were looking up an IP address, fill that */
  if (!ip && child->ip) {
    strcpy(result.ip, child->ip);
    ip = result.ip;
  }

  /* If DNS failed but we have an IP, fill the name with the IP */
  if (ip && (!name || !strlen(name))) {
    strncpy(result.name, ip, sizeof(result.name));
    result.name[sizeof(result.name) - 1] = '\0';

    debug("%d: Changed name to '%s'", pid, result.name);
    name = result.name;
  }

  /* Call the function */
  child->function(child->boundto, child->data, ip, name);

  /* Clean up */
  close(child->pipe);
  free(child);

  return 1;
}

/* Kill off any children associated with an ircproxy */
int dns_delall(void *b) {
  struct dnschild *c, *l;
  int numdone;

  l = 0;
  c = dnschildren;
  numdone = 0;
  
  while (c) {
    if (c->boundto == b) {
      struct dnschild *n;

      n = c->next;
      debug("Killing DNS process %d", c->pid);
      kill(c->pid, SIGKILL);
      close(c->pipe);
      free(c);

      if (l) {
        c = l->next = n;
      } else {
        c = dnschildren = n;
      }
    } else {
      l = c;
      c = c->next;
    }
  }

  return numdone;
}

/* Kill off ALL dns children */
void dns_flush(void) {
  struct dnschild *c;

  c = dnschildren;
  while (c) {
    struct dnschild *n;

    n = c->next;
    debug("Killing DNS process %d", c->pid);
    kill(c->pid, SIGKILL);
    close(c->pipe);
    free(c);
    c = n;
  }

  dnschildren = 0;
}

/* Returns the IP address of a hostname */
int dns_addrfromhost(void *boundto, void *data, const char *name, dns_fun_t function) {
  return _dns_startrequest(boundto, function, data, 0, name);
}

/* Returns the hostname of an IP address */
int dns_hostfromaddr(void *boundto, void *data, const char *ip, dns_fun_t function) {
  return _dns_startrequest(boundto, function, data, ip, 0);
}

/* Fill a sockaddr_in from a hostname or hostname:port combo thing */
int dns_filladdr(void *boundto, const char *name, const char *defaultport,
                 SOCKADDR *result, dns_fun_t function) {
  int ret = 0;
  
  char host[DNS_MAX_HOSTLEN];
  char portbuf[32];
  int port;

  memset(result, 0, sizeof(SOCKADDR));
  host[0] = '\0';
  /* 1. IPv6 [addr]:port */
  if ((sscanf(name, "[%39[^]]]:%31s", host, portbuf) == 2) ||
      /* 2. host/ipv4:port */
      (sscanf(name, "%255[^:]:%31s", host, portbuf) == 2))
    port = dns_portfromserv(portbuf);
  else {
    /* 3. just host name */
    port = dns_portfromserv(defaultport);
    strncpy(host, name, sizeof(host));
    host[sizeof(host) - 1] = '\0';
  }

  ret = _dns_startrequest(boundto, function, (void*)port, 0, host);

  return ret;
}

/* Returns a network port number for a port as a string */
int dns_portfromserv(const char *serv) {
  struct servent *entry;

  entry = getservbyname(serv, "tcp");
  return (entry ? entry->s_port : htons(atoi(serv)));
}

/* Returns a service name for a network port number */
char *dns_servfromport(int port) {
  struct servent *entry;
  char *str;

  entry = getservbyport(port, "tcp");
  if (entry) {
    str = x_strdup(entry->s_name);
  } else {
    str = x_sprintf("%d", port);
  }

  return str;
}

/* look up hostname, place string form of address into ip.
 * ip must be long enough to hold the result: IPv6:40, v4:16
 * Returns 1 on success */
int dns_getip(const char *name, char *ip) {
  int isip;

  /* save lookup time if name is already in IP form? */
#ifdef HAVE_IPV6
  struct addrinfo *head, hints;
  int ret = 0;

  union {
    struct in_addr v4addr;
    struct in6_addr v6addr;
  } addr_u;
  
  if (inet_pton(AF_INET, name, &addr_u.v4addr) <= 0)
    isip = inet_pton(AF_INET6, name, &addr_u.v6addr) > 0 ? 1 : 0;
  else
    isip = 1;
#else
  struct in_addr inp;
  struct hostent *host;
  
  isip = inet_aton(name, &inp);
#endif
  
  if (isip) {
    strcpy(ip, name);
    return 1;
  }

#ifdef HAVE_IPV6
  head = NULL;
  memset (&hints, 0, sizeof (hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  getaddrinfo (name, NULL, &hints, &head);
  
  if (head)
  {
    if (getnameinfo(head->ai_addr, head->ai_addrlen, ip, 40, NULL, 
                    0, NI_NUMERICHOST) == 0)
      ret = 1;
    freeaddrinfo (head);
  }
  
  return ret;
#else
  host = gethostbyname(name);
  if (host)
  {
    char *temp = inet_ntoa(*(struct in_addr *)host->h_addr);
    strcpy(ip, temp);
    
    return 1;
  }
  
  return 0;
#endif
}

/* look up ip, place up to len bytes of name into name.
* Returns 1 on success */
int dns_getname(const char *ip, char *name, int len) {
#ifdef HAVE_IPV6
  struct addrinfo *head = NULL, hints;
  int ret = 0;
  
  memset (&hints, 0, sizeof (hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_CANONNAME;
  getaddrinfo (ip, NULL, &hints, &head);
  
  if (head)
  {
    if (getnameinfo(head->ai_addr, head->ai_addrlen, name, len, NULL, 
                    0, NI_NAMEREQD) == 0)
      ret = 1;
    
    freeaddrinfo (head);
  }
  
  return ret;
#else
  struct hostent *host;
  struct in_addr addr;
  
  if (inet_aton (ip, &addr)) {
    host = gethostbyaddr((const char*)&addr, sizeof(addr), AF_INET);
    if (host) {
      strncpy(name, host->h_name, len);
      name[len - 1] = '\0';

      return 1;
    }
  }
  
  return 0;
#endif
}
